#pragma once
enum eFileAction {FILE_ACTION_NONE, FILE_ACTION_CREATE, FILE_ACTION_DELETE, FILE_ACTION_CHANGE,};
enum eFileMode {FILE_MODE_READ, FILE_MODE_WRITE,};
enum eFileType {FILE_TYPE_BINARY, FILE_TYPE_TEXT,};

typedef DWORD DIR_NOTIFICATION_CALLBACK(class CFileData*, eFileAction, void*);

class CFileData
{
	friend class CDirectory;
	class CDirectory		*	m_pDirectory;
	char						m_szFilePath[MAX_PATH];
	const char			*	m_szFileExt;
	const char			*	m_szFileName;
	__int64					m_nLastModifiedTime;
	WIN32_FIND_DATAA			m_FindData;

public:
	CFileData(const char * szFilePath, CDirectory * pDir, WIN32_FIND_DATAA * pFindData = nullptr);

	inline CDirectory				*	GetDirectory() const							{return m_pDirectory;}
	inline const char				*	GetFilePath() const							{return m_szFilePath;}
	inline const char				*	GetFileName() const							{return m_szFileName;}
	inline const char				*	GetFileExt()	 const							{return m_szFileExt;}
	inline const WIN32_FIND_DATAA	*	GetFindData() const							{return &m_FindData;}
	inline bool							IsNamed(const char * szName) const			{return *GetFileName() ? !strcmp(GetFileName(), szName) : false;}
	char						*	GetName(char * pOut, size_t len)
	{
		size_t size = GetFileExt() - GetFileName();
		return szncpy(pOut, GetFileName(), len >= size ? size : len);
	}
	bool							IsExtension(const char * szExt) const
	{
		return GetFileExt() ? !strcmp(szExt, GetFileExt()) : false;
	}
};

class CDirectory : public CLListEx<CFileData*>
{
	friend class CDirectoryManager;

	HANDLE				m_hDirectoryChanges;
	char					m_szDirPath[MAX_PATH];
	char				*	m_szDirName;
	DWORD				m_dwHash;
	WIN32_FIND_DATAA		m_FindData;
	
	DIR_NOTIFICATION_CALLBACK * m_pFunc;

public:
	CDirectory(const char * szDirPath, const char * szFilePattern="*")
	{
		strcpy(m_szDirPath, szDirPath);
		m_szDirName = strrchr(m_szDirPath, '\\') + 1;
		
		// Generate a hash for this directory so the manager doesn't create duplicates
		m_dwHash = GenerateHash(m_szDirPath);

		m_hDirectoryChanges = CreateFile(szDirPath, FILE_LIST_DIRECTORY, FILE_SHARE_READ|FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
				
		if(szFilePattern) FindFiles(szFilePattern);
	}
	~CDirectory()
	{
		CloseHandle(m_hDirectoryChanges);
	}

	inline const char * GetPath() const								{return m_szDirPath;}
	DWORD GetHash() const											{return m_dwHash;}

	void FindFiles(const char * szFilePattern="*")
	{
		std::string pattern = m_szDirPath;
		pattern += "\\";
		pattern += szFilePattern;
		if(HANDLE hFind = FindFirstFile(pattern.c_str(), &m_FindData))
		{
			do
			{
				if(!(m_FindData.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN))
				{
					if(!(m_FindData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY))
					{
						if(!(m_FindData.dwFileAttributes & FILE_ATTRIBUTE_DEVICE))
						{
							if(!GetFileDataWithName(m_FindData.cFileName))
								Add(new CFileData(m_FindData.cFileName, this, &m_FindData));
						}
					}
				}
			}
			while(FindNextFile(hFind, &m_FindData));
			FindClose(hFind);
		}
	}

	void SetHandler(DIR_NOTIFICATION_CALLBACK * pFunc)
	{
		m_pFunc = pFunc;
	}

	void UpdateNotifications()
	{
		FILE_NOTIFY_INFORMATION fni[1024];
		DWORD dwBytes;
		memset(&fni, 0, sizeof(fni));

		FILE_NOTIFY_INFORMATION * pfni = fni;

		for(DWORD dwOffset=0; ReadDirectoryChangesW(m_hDirectoryChanges, &fni, sizeof(fni), false, FILE_NOTIFY_CHANGE_SECURITY
																					| FILE_NOTIFY_CHANGE_CREATION
																					| FILE_NOTIFY_CHANGE_LAST_ACCESS
																					| FILE_NOTIFY_CHANGE_LAST_WRITE
																					| FILE_NOTIFY_CHANGE_SIZE
																					| FILE_NOTIFY_CHANGE_ATTRIBUTES
																					| FILE_NOTIFY_CHANGE_DIR_NAME
																					| FILE_NOTIFY_CHANGE_FILE_NAME,
																					&dwBytes, NULL, NULL); dwOffset += dwBytes)
		{
			// (BUG): For some reason, any code after this for loop wont run, causing a need for some bad code...
			char * szStr = pfni->FileNameLength ? new char[pfni->FileNameLength] : nullptr;
			if(!szStr || !*pfni->FileName) continue;
			wcstombs(szStr, pfni->FileName, pfni->FileNameLength);
			auto pFileData = GetFileDataWithName(szStr);
			if(!pFileData)
			{
				pFileData = new CFileData(szStr, this);
				Add(pFileData);
			}
			switch(pfni->Action)
			{
			case FILE_ACTION_ADDED:
			case FILE_ACTION_MODIFIED:
			case FILE_ACTION_RENAMED_NEW_NAME:
				pFileData->m_nLastModifiedTime = GetTime();
				break;
			}
			delete szStr;
			pfni = (FILE_NOTIFY_INFORMATION*)((BYTE*)fni + dwOffset);
		}

		FindNextChangeNotification(m_hDirectoryChanges);
	}

	CFileData * GetFileDataWithName(const char * szName)
	{
		for(auto pLink = Begin(); pLink != Bad(); ++pLink)
		{
			if(pLink->IsNamed(szName))
				return *pLink;
		}
		return nullptr;
	}

	/*static DWORD WINAPI WorkThread(void * pData)
	{
		HANDLE hDirChanges = (HANDLE)pData;
		WaitForSingleObject(hDirChanges, 0);
		FindNextChangeNotification(hDirChanges);
	}*/
};

class CFile
{
	FILE			*	m_pFile;
	CFileData		*	m_pData;
	CDirectory		*	m_pDirectory;
	int					m_nCurrentLine;
	size_t				m_nCurrentOffset;
	size_t				m_nSize;
	__int64				m_nLastModifiedTime;
	eFileMode			m_eFileMode;
	char				m_szFilePath[MAX_PATH];
	char				m_szFileDir[MAX_PATH];
	const char	*		m_szFileName;

	DWORD CALLBACK		IsFileModified(void * pData)
	{
	}

	void				Init(const char * szFilePath, eFileMode mode, eFileType type)
	{
		const char * szOpenMode;

		if(mode == FILE_MODE_WRITE)
		{
			m_eFileMode = FILE_MODE_WRITE;
			szOpenMode = type == FILE_TYPE_BINARY ? "wb" : "wt";
		}
		else if(mode == FILE_MODE_READ)
		{
			m_eFileMode = FILE_MODE_READ;
			szOpenMode = type == FILE_TYPE_BINARY ? "rb" : "rt";
		}

		m_nLastModifiedTime = GetFileModifiedTime(szFilePath);
		
		if(m_pFile = OpenFile(szFilePath, szOpenMode))
		{
			const char * pStr = strrchr(szFilePath, '\\');
			GetWorkingDir(m_szFilePath, sizeof(m_szFilePath));
			strcat(m_szFilePath, "\\");
			strcat(m_szFilePath, szFilePath);
			GetFileDirectory(m_szFilePath, m_szFileDir);
			strcat(m_szFileDir, "\\");
			
			m_szFileName = m_szFilePath[strlen(m_szFileDir)] ? &m_szFilePath[strlen(m_szFileDir)] : nullptr;
			
			m_nSize = FileGetSize(m_pFile);
			m_nCurrentOffset = 0;
			m_nCurrentLine = 0;
		}
		else
		{
			m_nSize = -1;
		}
	}

public:
	CFile(const char * szFilePath, eFileMode mode, eFileType type = FILE_TYPE_BINARY) :
		m_pData(nullptr)
	{
		Init(szFilePath, mode, type);
	}
	CFile(CDirectory * pDir, const char * szFilePath, eFileMode mode, eFileType type = FILE_TYPE_BINARY) : m_pDirectory(pDir),
		m_pData(nullptr)
	{
		Init(szFilePath, mode, type);
		if(!OK()) return;
		
		for(auto pLink = m_pDirectory->Begin(); pLink != m_pDirectory->Bad(); ++pLink)
		{
			if(!strcmp(m_szFileName, pLink->GetFileName()))
			{
				m_pData = *pLink;
				break;
			}
		}
	}
	~CFile()
	{
		if(m_pFile) CloseFile(m_pFile);
	}

	bool		WriteLine(const char * szStr)
	{
		if(m_eFileMode != FILE_MODE_WRITE) Exception("Attempt to write to a read-only file (internal error)");
		return WriteStringToFile(m_pFile, szStr);
	}
	bool		WriteLineFormat(const char * szFormat, ...)
	{
		va_list args;
		va_start(args, szFormat);
		vsprintf(g_szTempString, szFormat, args);
		va_end(args);
		if(m_eFileMode != FILE_MODE_WRITE) Exception("Attempt to write to a read-only file (internal error)");
		return WriteStringToFile(m_pFile, g_szTempString);
	}
	char	*	ReadLine(char * szLineOut, int nSize)
	{
		if(m_eFileMode != FILE_MODE_READ) Exception("Attempt to read from a write-only file (internal error)");
		if(ReadStringFromFile(m_pFile, szLineOut, nSize)) m_nCurrentLine++;
		else return nullptr;
		return szLineOut;
	}
	size_t		Read(void *pBuff, int nLen)
	{
		if(m_eFileMode != FILE_MODE_READ) Exception("Attempt to read from a write-only file (internal error)");
		return ReadBytesFromFile(m_pFile, pBuff, nLen);
	}
	size_t		Write(const void *pBuff, int nLen)
	{
		if(m_eFileMode != FILE_MODE_WRITE) Exception("Attempt to write to a read-only file (internal error)");
		return WriteBytesToFile(m_pFile, pBuff, nLen);
	}

	template<typename T>	size_t	Write(T val)	{return WriteToFile<T>(m_pFile, val);}
	template<typename T>	T		Read() const	{return ReadFromFile<T>(m_pFile);}

	inline const char	*	GetName() const			{return *m_szFileName ? m_szFileName : nullptr;}
	inline const char	*	GetPath() const			{return *m_szFilePath ? m_szFilePath : nullptr;}
	inline int				GetLineNumber()	const	{return m_nCurrentLine;}
	inline bool				HasEnded() const		{return IsEndOfFile(m_pFile);}
	inline size_t			GetPos()				{return m_nCurrentOffset = FileGetCursorPos(m_pFile);}
	inline size_t			GetPos() const			{return FileGetCursorPos(m_pFile);}
	inline size_t			GetSize() const			{return m_nSize;}
	inline const __int64	GetModifiedTime() const	{
		return m_pData ? FiletimeToInt64(m_pData->GetFindData()->ftLastWriteTime)  : m_nLastModifiedTime;
	}
	inline bool				OK() const				{return m_pFile ? true : false;}
};

class CDirectoryManager
{
	CList<CDirectory>			m_DirectoryList;
	CLListEx<CDirectory*>		m_Directories;
	DIR_NOTIFICATION_CALLBACK	m_pNotificationFunc;
	HANDLE						m_hManagementThread;

	static DWORD WINAPI ManageDirectories(void * pData);

public:
	CDirectoryManager()
	{
		m_hManagementThread = CreateThread(nullptr, 0, &ManageDirectories, nullptr, NULL, nullptr);
	}
	~CDirectoryManager()
	{
	 	CloseHandle(m_hManagementThread);
	}

	CDirectory * Add(const char * szDirPath, const char * szFilePattern="*")
	{
		static char szTemp[MAX_PATH];

		if(strchr(szDirPath, ':')) strcpy(szTemp, szDirPath);
		else
		{
			GetWorkingDir(szTemp, sizeof(szTemp));
			strcat(szTemp, "\\");
			strcat(szTemp, szDirPath);
		}

		size_t len = strlen(szTemp);

		// If \ is at the end, remove it
		while(szTemp[len-- - 1] == '\\');
		szTemp[len] = '\0';

		// No point creating CDirectory's to manage the same dir, check for a currently managed dir...
		DWORD dwHash = GenerateHash(szDirPath, len);
		if(auto pDir = m_DirectoryList.Get(dwHash))
		{
			do pDir = m_DirectoryList.Next();
			while(pDir && strcmp(pDir->GetPath(), szTemp));
			
			// Even if the filter is the same, we may still need to update the search...
			pDir->FindFiles(szFilePattern);
			return pDir;
		}

		// Create the dir
		auto pDir = new CDirectory(szTemp, szFilePattern);
		m_Directories.Add(pDir);
		m_DirectoryList.Add(pDir->GetHash(), pDir);
		return pDir;
	}
};

extern CDirectoryManager DirManager;